package com.softaria.spkiller.analyzer.hierarchy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;

import java.util.Map;

import com.softaria.spkiller.dependencies.DependencyGraph;
import com.softaria.spkiller.metadata.ClassIdentifier;

public class BrokenHierarchyReporter<M> {

	static class Node {
		private Node parent;
		private Collection<ClassIdentifier> children;

		private ClassIdentifier classIdentifier;

		public Node(ClassIdentifier classIdentifier) {
			super();
			this.classIdentifier = classIdentifier;
		}

		public Node getParent() {
			return parent;
		}

		public void setParent(Node parent) {
			this.parent = parent;
		}

		public Collection<ClassIdentifier> getChildrenIds() {
			return children;
		}

		public void setChildren(Collection<ClassIdentifier> children) {
			this.children = children;
		}

		public ClassIdentifier getClassIdentifier() {
			return classIdentifier;
		}

		@Override
		public int hashCode() {
			return classIdentifier.hashCode();
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof Node) {
				Node n2 = (Node) obj;
				return classIdentifier.equals(n2.classIdentifier);
			} else {
				return false;
			}
		}

	}

	class HierarchyImpl implements Hierarchy<M> {

		class HierarchyNodeWrapper implements HierarchyNode<M> {

			private Node node;
			private Collection<HierarchyNode<M>> children;
			private HierarchyNode<M> root;

			private M marker;

			public HierarchyNodeWrapper(Node node) {
				super();
				this.node = node;
			}

			@Override
			public HierarchyNode<M> getParent() {
				if (root == null) {
					if (node == null) {
						root = null;
					} else {
						if (node.getParent() == null) {
							root = null;
						} else {
							root = getHierarchyNode(node.getParent()
									.getClassIdentifier());
						}
					}

				}
				return root;
			}

			@Override
			public Collection<HierarchyNode<M>> getChildren() {
				if (children == null) {

					Collection<ClassIdentifier> childrenIDs = node
							.getChildrenIds();

					children = new ArrayList<HierarchyNode<M>>(
							childrenIDs == null ? 0 : childrenIDs.size());

					if (childrenIDs != null) {

						for (ClassIdentifier childID : node.getChildrenIds()) {
							HierarchyNode<M> hn = getHierarchyNode(childID);
							children.add(hn);
						}
					}

				}

				return children;
			}

			@Override
			public ClassIdentifier getClassIdentifier() {
				return node.getClassIdentifier();
			}

			@Override
			public void setMarker(M marker) {
				this.marker = marker;

			}

			@Override
			public M getMarker() {
				return marker;
			}

			@Override
			public String toString() {
				return "Node: [" + node.getClassIdentifier() + "]";
			}

		}

		private Map<ClassIdentifier, Node> allNode;
		private Map<ClassIdentifier, HierarchyNodeWrapper> allHierarchyNode = new HashMap<ClassIdentifier, HierarchyNodeWrapper>();
		private HierarchyNodeWrapper root;

		public HierarchyNodeWrapper getHierarchyNode(ClassIdentifier classID) {
			HierarchyNodeWrapper node = allHierarchyNode.get(classID);
			if (node == null) {
				Node n = allNode.get(classID);
				if (n != null) {
					if (root == null || isAncestor(n, root.node)) {
						node = new HierarchyNodeWrapper(n);
						allHierarchyNode.put(classID, node);
					}
				}
			}
			return node;
		}

		private boolean isAncestor(Node son, Node father) {
			Node parent = son.getParent();
			if (parent == null) {
				return false;
			}

			if (parent.getClassIdentifier().equals(father.getClassIdentifier())) {
				return true;
			}

			return isAncestor(parent, father);
		}

		public HierarchyImpl(Map<ClassIdentifier, Node> allNodes, Node root) {
			this.allNode = allNodes;
			this.root = getHierarchyNode(root.getClassIdentifier());
		}

		@Override
		public HierarchyNode<M> getRoot() {
			return root;
		}

		@Override
		public Collection<ClassIdentifier> getAll() {
			Collection<ClassIdentifier> ret = new ArrayList<ClassIdentifier>();

			ret.add(getRoot().getClassIdentifier());

			addAllChildren(getRoot(), ret);

			return ret;
		}

		private void addAllChildren(HierarchyNode<M> node,
				Collection<ClassIdentifier> ret) {
			for (HierarchyNode<M> child : node.getChildren()) {
				ret.add(child.getClassIdentifier());

				addAllChildren(child, ret);
			}

		}

	}

	public Collection<Hierarchy<M>> checkHierarchy(
			DependencyGraph dependencyGraph,
			Collection<ClassIdentifier> classes,
			HierarchyProblemCollector collector) {

		Map<ClassIdentifier, Node> allNodes = new HashMap<ClassIdentifier, BrokenHierarchyReporter.Node>();

		if (classes != null) {
			for (ClassIdentifier aClass : classes) {
				Node node = allNodes.get(aClass);

				if (node == null) {
					node = new Node(aClass);
					node.setParent(null);

					allNodes.put(aClass, node);

				} else {
					if (node.getParent() == null) {
						// if it exists it must be added as somebodie's child
						// otherwise it means it happens twice in "classes"
						throw new RuntimeException();
					}
				}

				addChildren(dependencyGraph, allNodes, classes, node, collector);

			}
		}

		Collection<Hierarchy<M>> hierarchies = new ArrayList<Hierarchy<M>>();

		for (Node node : allNodes.values()) {
			if (node.getParent() == null) {
				Hierarchy<M> h = new HierarchyImpl(allNodes, node);
				hierarchies.add(h);
			}
		}

		if (hierarchies.size() == 0) {
			collector.reportNoHierarchyRoot();
		} else {

			// Check if there any cycle here
			// We still can have 1 or more hierarchies + 1 or more cycles (with
			// no root)

			HashSet<ClassIdentifier> allClasses = new HashSet<ClassIdentifier>(
					classes);

			for (Hierarchy<M> h : hierarchies) {
				allClasses.removeAll(h.getAll());
			}

			if (!allClasses.isEmpty()) {
				collector.reportCycle(allClasses);
			}

			if (hierarchies.size() > 1) {
				collector.reportMultipleHierarchies(hierarchies.size());
			}
		}

		return hierarchies;

	}

	private void addChildren(DependencyGraph dependencyGraph,
			Map<ClassIdentifier, Node> allNodes,
			Collection<ClassIdentifier> classes, Node node,
			HierarchyProblemCollector collector) {

		if (node.getChildrenIds() != null) {
			// ERROR - also not possible
			throw new RuntimeException();
		}

		Collection<ClassIdentifier> children = dependencyGraph
				.getUsedClasses(node.getClassIdentifier());

		// children.retainAll(classes);

		node.setChildren(children);

		for (ClassIdentifier child : children) {
			Node childNode = allNodes.get(child);

			if (childNode != null) {

				if (childNode.getParent() == null) {
					childNode.setParent(node);
				} else {
					if (!childNode.getParent().equals(node)) {
						// ERROR - this node already has a
						// parent , different from our one
						if (classes.contains(child)) {
							collector.reportBrokenHierarchy(child, childNode
									.getParent().getClassIdentifier(), node
									.getClassIdentifier());
						}
						// else it is not a part of our hierarchy!
					}
				}
			} else {
				childNode = new Node(child);
				childNode.setParent(node);
				childNode.setChildren(null);

				allNodes.put(child, childNode);
			}
		}
	}

}
